using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Management;
using System.Text;
using System.Text.RegularExpressions;
using EnvDTE;
using EnvDTE80;
using TeamReview.UI;

namespace TeamReview.Command
{
    /// <summary>
    /// Grabs the current highlighted <see cref="CodeSelection"/> and  
    /// shows <see cref="CreateWorkItemForm"/>
    /// </summary>
    /// <remarks>
    /// Template Pattern implementor of <see cref="ShowFormCommand"/>
    /// </remarks>
    public class CreateWorkItemCommand : ShowFormCommand
    {
        private enum LeadingCountType { None, Spaces, Tabs }

        private const string SPACE_REGEX = "^ *[^ ]";
        private const string TAB_REGEX = "^\t*[^\t]";
        private CreateWorkItemForm _form;



        public CreateWorkItemCommand(DTE2 visualStudio) : base(visualStudio)
        {
        }

        #region Private Static "Helper" Methods
        /// <summary>
        /// Get the relative file path of <see cref="absoluteFilePath"/> from <see cref="mainDirPath"/>
        /// </summary>
        /// <remarks>Used to find a code file's relative path from it's project file</remarks>
        /// <param name="mainDirPath">The reference location</param>
        /// <param name="absoluteFilePath">the location of a file to find the relative path to from <paramref name="mainDirPath"/></param>
        /// <returns>the relative path from <see cref="mainDirPath"/> to <see cref="absoluteFilePath"/></returns>
        private static string EvaluateRelativePath(string mainDirPath, string absoluteFilePath)
        {
            if (String.IsNullOrEmpty(mainDirPath))
                return Path.GetFullPath(absoluteFilePath);

            string[] firstPathParts = Path.GetDirectoryName(mainDirPath).Trim(Path.DirectorySeparatorChar).Split(Path.DirectorySeparatorChar);
            string[] secondPathParts = absoluteFilePath.Trim(Path.DirectorySeparatorChar).Split(Path.DirectorySeparatorChar);

            int sameCounter = 0;
            for (int i = 0; i < Math.Min(firstPathParts.Length, secondPathParts.Length); i++)
            {
                if (!firstPathParts[i].ToLower().Equals(secondPathParts[i].ToLower()))
                    break;

                sameCounter++;
            }

            if (sameCounter == 0)
                return absoluteFilePath;


            string newPath = String.Empty;
            for (int i = sameCounter; i < firstPathParts.Length; i++)
            {
                if (i > sameCounter)
                    newPath += Path.DirectorySeparatorChar;
                newPath += "..";
            }

            for (int i = sameCounter; i < secondPathParts.Length; i++)
            {
                newPath += Path.DirectorySeparatorChar;
                newPath += secondPathParts[i];
            }

            return newPath;
        }



        /// <summary>
        /// Removes a consistent leading idententation from each line in order to left justify a snippet of code
        /// </summary>
        /// <remarks>
        /// <para>
        /// If all lines within a code block have at-least 2 tabs (for example)
        /// of leading indentation but some have more than 2, then only 2 tabs will be
        /// removed from each line. The end result is that the text will be left justified
        /// within a text-area. 
        /// </para>
        /// <para>
        /// Works for leading tabs and leading spaces. The white-space choice is made
        /// by the first leading white space found within the passed in string.
        /// </para>
        /// </remarks>
        /// 
        /// <param name="p"></param>
        /// <returns></returns>
        private static string LeftJustify(string p)
        {
            string[] lines = p.Split('\n');
            LeadingCountType leadingType = LeadingCountType.None;
            int minIndent = -1;
            for (int i = 0; i < lines.Length; i++)
            {
                if (lines[i].Trim().Length > 0)
                {
                    int leadingCount = -1;
                    switch (leadingType)
                    {
                        case LeadingCountType.None:
                            int leadingSpaceCount = Regex.Match(lines[i], SPACE_REGEX).Length - 1;
                            int leadingTabCount = Regex.Match(lines[i], TAB_REGEX).Length - 1;
                            if (leadingSpaceCount > 0)
                            {
                                leadingCount = leadingSpaceCount;
                                leadingType = LeadingCountType.Spaces;
                            }
                            else if (leadingTabCount > 0)
                            {
                                leadingCount = leadingTabCount;
                                leadingType = LeadingCountType.Tabs;
                            }
                            break;
                        case LeadingCountType.Spaces:
                            leadingCount = Regex.Match(lines[i], SPACE_REGEX).Length - 1;
                            break;
                        default:
                            leadingCount = Regex.Match(lines[i], TAB_REGEX).Length - 1;
                            break;
                    }

                    //As soon as a line comes through with text and no leading indentation
                    //there is no reason to continue 
                    if (leadingCount == 0)
                        return p;

                    if (minIndent == -1)
                        minIndent = leadingCount;
                    else
                        minIndent = Math.Min(leadingCount, minIndent);
                }
            }

            if (minIndent <= 0)
                return p;

            StringBuilder builder = new StringBuilder(p.Length);
            for (int i = 0; i < lines.Length; i++)
            {
                if (lines[i].Trim().Length > 0)
                    builder.Append(String.Format("{0}\n", lines[i].Substring(minIndent, lines[i].Length - minIndent)));
                else
                    //keep the \r
                    builder.Append(String.Format("{0}\n", lines[i]));
            }
            return builder.ToString();
        }

        /// <summary>
        /// For a given <see cref="CodeElement"/> the <see cref="CodeSelection"/> tree is "walked up"
        /// to find parent values, such as the parent class for a method, or the parent namespace for a class.
        /// </summary>
        /// <param name="cs"></param>
        /// <param name="ce"></param>
        /// <returns></returns>
        private static CodeSelection GetMemberParentValues(CodeSelection cs, CodeElement ce)
        {
            CodeElement parent = ce;

            bool canTraverse = true;

            //parent.Kind is a Null Reference sometimes

            while (canTraverse)
            {
                if (parent == null)
                {
                    break;
                }
                try
                {
                  switch (parent.Kind)
                  {
                    case vsCMElement.vsCMElementFunction:
                      CodeFunction func = (CodeFunction2)parent;
                      parent = (CodeElement2)func.Parent;
                      break;
                    case vsCMElement.vsCMElementProperty:
                      CodeProperty prop = (CodeProperty)parent;
                      parent = (CodeElement)prop.Parent;
                      break;
                    case vsCMElement.vsCMElementVariable:
                      CodeVariable var = (CodeVariable)parent;
                      parent = (CodeElement)var.Parent;
                      break;
                    case vsCMElement.vsCMElementEvent:
                      CodeEvent evt = (CodeEvent)parent;
                      parent = (CodeElement)evt.Parent;
                      break;
                    case vsCMElement.vsCMElementClass:
                      cs.CodeClass = parent.FullName;
                      CodeClass cc = (CodeClass)parent;
                      parent = (CodeElement)cc.Parent;
                      break;
                    case vsCMElement.vsCMElementInterface:
                      cs.CodeClass = parent.FullName;
                      CodeInterface ci = (CodeInterface)parent;
                      parent = (CodeElement)ci.Parent;
                      break;
                    case vsCMElement.vsCMElementStruct:
                      cs.CodeClass = parent.FullName;
                      CodeStruct cst = (CodeStruct)parent;
                      parent = (CodeElement)cst.Parent;
                      break;
                    case vsCMElement.vsCMElementDelegate:
                      cs.CodeClass = parent.FullName;
                      CodeDelegate del = (CodeDelegate)parent;
                      parent = (CodeElement)del.Parent;
                      break;
                    case vsCMElement.vsCMElementEnum:
                      cs.CodeClass = parent.FullName;
                      CodeEnum enu = (CodeEnum)parent;
                      parent = (CodeElement)enu.Parent;
                      break;
                    case vsCMElement.vsCMElementNamespace:
                      cs.Namespace = parent.FullName;
                      canTraverse = false;
                      break;
                    default:
                      canTraverse = false;
                      break;
                  }
                }
                  //properties in interfaces cause some exceptions..argh
                catch
                {
                  parent = null;
                }
            }
            return cs;
        }


        /// <summary>
        /// Sets the <see cref="CodeSelection.CodeMember"/> value based on the passed in <see cref="CodeElement"/>
        /// </summary>
        /// <remarks>
        /// <para>
        /// Different types of <see cref="CodeSelection"/>'s such as Classes, Namespaces, Methods have their own
        /// appropriate properties for getting the most recognizable name for that member.
        /// </para>
        /// </remarks>
        /// <param name="cs"></param>
        /// <param name="ce"></param>
        /// <returns></returns>
        private static CodeSelection SetCodeMemberName(CodeSelection cs, CodeElement ce)
        {

          if (ce == null)
            return cs;

            if (!string.IsNullOrEmpty(ce.FullName))
            {
                cs.CodeMember = ce.FullName;
                return cs;
            }

            if (!string.IsNullOrEmpty(ce.Name))
            {
                cs.CodeMember = ce.Name;
                return cs;
            }

            if (!string.IsNullOrEmpty(ce.ProjectItem.Name))
            {
                cs.CodeMember = ce.ProjectItem.Name;
                return cs;
            }

            return cs;
        }

        /// <summary>
        /// Returns an adjusted CodeElement. Walks comments 'down' to the next real element.
        /// Works from XML Comment area above a method.
        /// </summary>
        /// <remarks>This code borrowed from Rick Stahl a long time ago.</remarks>
        /// <returns></returns>		

        [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
        private CodeElement GetCodeElement(CodeLanguage CodeLanguage, TextSelection selection)
        {
            EditPoint selPoint = selection.ActivePoint.CreateEditPoint();
            selPoint.StartOfLine();

            while (true)
            {
                string BlockText = selPoint.GetText(selPoint.LineLength).Trim();

                // *** Skip over any XML Doc comments and Attributes
                if (CodeLanguage == CodeLanguage.CSharp && BlockText.StartsWith("/// ") ||
                            CodeLanguage == CodeLanguage.CSharp && BlockText.StartsWith("[") ||
                            CodeLanguage == CodeLanguage.VB && BlockText.StartsWith("''' ") ||
                            CodeLanguage == CodeLanguage.VB && BlockText.StartsWith("<"))
                {
                    selPoint.LineDown(1);
                    selPoint.StartOfLine();
                }
                else
                    break;
            }

            // *** Make sure the cursor is placed inside of the definition always
            // *** Especially required for single line methods/fields/events etc.
            selPoint.EndOfLine();
            selPoint.CharLeft(1);  // Force into the text

            selPoint.GetText(selPoint.LineLength).Trim();

            // get the element under the cursor
            CodeElement element = null;
            FileCodeModel2 CodeModel = shellDTE.ActiveDocument.ProjectItem.FileCodeModel as FileCodeModel2;
            if (CodeModel == null)
            {
                return null;
            }

            // *** Supported scopes - set up here in the right parsing order
            // *** from lowest level to highest level
            // *** NOTE: Must be adjusted to match any CodeElements supported
            vsCMElement[] Scopes = { 
                                     vsCMElement.vsCMElementFunction,
                                     vsCMElement.vsCMElementProperty,
                                     vsCMElement.vsCMElementVariable,
                                     vsCMElement.vsCMElementEvent,
                                     vsCMElement.vsCMElementClass,
                                     vsCMElement.vsCMElementInterface,
                                     vsCMElement.vsCMElementStruct,
                                     vsCMElement.vsCMElementEnum
                                   };

            foreach (vsCMElement scope in Scopes)
            {
                // Catch and ignore all exceptions from CodeModel - CodeElementFromPoint will raise a COM based exception
                //System.Runtime.InteropServices.COMException (0x80004005): Unspecified error
                try
                {
                    element = CodeModel.CodeElementFromPoint(selPoint, scope);
                    if (element != null)
                        break; 
                }
                catch 
                {
                    ;
                }
            }
            return element;
        }
        #endregion



        /// <summary>
        /// Gets the highligted <see cref="CodeSelection"/> in the <see cref="DTE2.ActiveDocument"/>
        /// </summary>
        /// <returns></returns>
        private CodeSelection CreateCodeSelection()
        {
            CodeSelection cs = new CodeSelection();
            Document doc = shellDTE.ActiveDocument;
            EnvDTE.TextDocument textDoc = (TextDocument)doc.Object("");

            if (shellDTE.ActiveWindow.Document == null) return cs;

            TextSelection selection = (TextSelection)shellDTE.ActiveWindow.Document.Selection;

            cs.StartLine = selection.TopPoint.Line;
            cs.EndLine = selection.BottomPoint.Line;

            textDoc.Selection.GotoLine(cs.StartLine, false);
            EnvDTE.EditPoint startPoint = textDoc.CreateEditPoint(textDoc.Selection.ActivePoint);
            textDoc.Selection.GotoLine(cs.EndLine, true);
            EnvDTE.EditPoint endPoint = textDoc.CreateEditPoint(textDoc.Selection.ActivePoint);

            textDoc.Selection.MoveToPoint(startPoint, false);
            textDoc.Selection.MoveToPoint(endPoint, true);

            cs.Snippet = LeftJustify(textDoc.Selection.Text);


            if (doc.ProjectItem == null || doc.ProjectItem.ContainingProject == null || String.IsNullOrEmpty(doc.ProjectItem.ContainingProject.FullName))
            {
                string solutionPath = shellDTE.Solution.FileName;
                solutionPath = Path.GetDirectoryName(solutionPath) + Path.DirectorySeparatorChar;
                cs.CodeFile = EvaluateRelativePath(solutionPath, doc.FullName);
            }
            else
            {
                cs.CodeFile = EvaluateRelativePath(doc.ProjectItem.ContainingProject.FullName, doc.FullName);
                cs.CodeProject = doc.ProjectItem.ContainingProject.UniqueName;
            }


            //Xml doesn't have a CodeLanguage
            CodeLanguage language = (CodeLanguage)Enum.Parse(typeof(CodeLanguage), textDoc.Language, true);
            CodeElement ce = GetCodeElement(language, selection);
            cs = SetCodeMemberName(cs, ce);
            cs = GetMemberParentValues(cs, ce);

            return cs;
        }

        /// <summary>
        /// Shows the Create workitem form 
        /// <see cref="CreateWorkItemForm"/>
        /// </summary>
        protected override void ShowForm()
        {
            if (_form != null)
                Shutdown();

            _form = new CreateWorkItemForm(shellDTE, CreateCodeSelection());
            _form.Show();
        }

        /// <summary>
        /// Close our form and marked for disposal
        /// </summary>
        public override void Shutdown()
        {
            if (_form != null)
            {
                _form.Close();
                _form.Dispose();
            }
        }
    }
}
